Long only 1/n portfolio#

import pandas as pd
pd.options.plotting.backend = "plotly"

import yfinance as yf

from cvx.simulator.builder import builder
from cvx.simulator.grid import resample_index

# Get rid of findfont: Font family 'Arial' not found.
# when running a remote notebook on Jupyter Server on Ubuntu Linux server
import logging
logging.getLogger("matplotlib.font_manager").setLevel(logging.ERROR)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[1], line 2
      1 import pandas as pd
----> 2 pd.options.plotting.backend = "plotly"
      4 import yfinance as yf
      6 from cvx.simulator.builder import builder

File ~/work/cvxjson/cvxjson/.venv/lib/python3.10/site-packages/pandas/_config/config.py:224, in DictWrapper.__setattr__(self, key, val)
    221 # you can't set new keys
    222 # can you can't overwrite subtrees
    223 if key in self.d and not isinstance(self.d[key], dict):
--> 224     _set_option(prefix, val)
    225 else:
    226     raise OptionError("You can only set the value of existing options")

File ~/work/cvxjson/cvxjson/.venv/lib/python3.10/site-packages/pandas/_config/config.py:160, in _set_option(*args, **kwargs)
    158 o = _get_registered_option(key)
    159 if o and o.validator:
--> 160     o.validator(v)
    162 # walk the nested dict
    163 root, k = _get_root(key)

File ~/work/cvxjson/cvxjson/.venv/lib/python3.10/site-packages/pandas/core/config_init.py:643, in register_plotting_backend_cb(key)
    640     return
    641 from pandas.plotting._core import _get_plot_backend
--> 643 _get_plot_backend(key)

File ~/work/cvxjson/cvxjson/.venv/lib/python3.10/site-packages/pandas/plotting/_core.py:1862, in _get_plot_backend(backend)
   1859 if backend_str in _backends:
   1860     return _backends[backend_str]
-> 1862 module = _load_backend(backend_str)
   1863 _backends[backend_str] = module
   1864 return module

File ~/work/cvxjson/cvxjson/.venv/lib/python3.10/site-packages/pandas/plotting/_core.py:1831, in _load_backend(backend)
   1826     if hasattr(module, "plot"):
   1827         # Validate that the interface is implemented when the option is set,
   1828         # rather than at plot time.
   1829         return module
-> 1831 raise ValueError(
   1832     f"Could not find plotting backend '{backend}'. Ensure that you've "
   1833     f"installed the package providing the '{backend}' entrypoint, or that "
   1834     "the package has a top-level `.plot` method."
   1835 )

ValueError: Could not find plotting backend 'plotly'. Ensure that you've installed the package providing the 'plotly' entrypoint, or that the package has a top-level `.plot` method.
data = yf.download(tickers = "SPY AAPL GOOG MSFT",  # list of tickers
                   period = "10y",                   # time period
                   interval = "1d",                 # trading interval
                   prepost = False,                 # download pre/post market hours data?
                   repair = True)                   # repair obvious price errors e.g. 100x?
[*********************100%***********************]  4 of 4 completed
prices = data["Adj Close"]
capital = 1e6
b = builder(prices=prices, initial_cash=capital)

for time, state in b:
    # each day we invest a quarter of the capital in the assets
    b[time[-1]] = 0.25 * state.nav / state.prices
portfolio = b.build()
portfolio.profit.cumsum().plot()
portfolio.nav.plot()

Rebalancing#

Usually we would not execute on a daily basis but rather rebalance every week, month or quarter. There are two approaches to deal with this problem in cvxsimulator.

  • Resample the existing daily portfolio (helpful to see effect of your hesitated trading)

  • Trade only on days that are within a predefined grid (most flexible if you have a rather irregular grid)

Resample an existing portfolio#

portfolio_resampled = portfolio.resample(rule="M")
frame = pd.DataFrame({"original": portfolio.nav, "monthly": portfolio_resampled.nav})
frame
original monthly
Date
2013-05-30 1.000000e+06 1.000000e+06
2013-05-31 9.945921e+05 9.945921e+05
2013-06-03 1.000400e+06 1.000391e+06
2013-06-04 9.917402e+05 9.917309e+05
2013-06-05 9.846364e+05 9.846127e+05
... ... ...
2023-05-23 7.505451e+06 7.493060e+06
2023-05-24 7.461416e+06 7.447306e+06
2023-05-25 7.603349e+06 7.590982e+06
2023-05-26 7.711937e+06 7.698989e+06
2023-05-30 7.735463e+06 7.721169e+06

2518 rows × 2 columns

print(portfolio_resampled.stocks)
                    AAPL          GOOG         MSFT          SPY
Date                                                            
2013-05-30  17854.390520  11527.266005  8564.268967  1814.033248
2013-05-31  17854.390520  11527.266005  8564.268967  1814.033248
2013-06-03  17895.614489  11573.477982  8432.882983  1831.100869
2013-06-04  17895.614489  11573.477982  8432.882983  1831.100869
2013-06-05  17895.614489  11573.477982  8432.882983  1831.100869
...                  ...           ...          ...          ...
2023-05-23  10590.586692  16651.905094  5882.633509  4316.566726
2023-05-24  10590.586692  16651.905094  5882.633509  4316.566726
2023-05-25  10590.586692  16651.905094  5882.633509  4316.566726
2023-05-26  10590.586692  16651.905094  5882.633509  4316.566726
2023-05-30  10590.586692  16651.905094  5882.633509  4316.566726

[2518 rows x 4 columns]
# almost hard to see that difference between the original and resampled portfolio
frame.plot()
# number of shares traded
portfolio_resampled.trades_stocks.iloc[1:].plot()

Trade only days in predefined grid#

b = builder(prices=prices, initial_cash=capital)

# define a grid
grid = resample_index(prices.index, rule="M")

for time, state in b:
    # each day we invest a quarter of the capital in the assets
    if time[-1] in grid:
        b[time[-1]] = 0.25 * state.nav / state.prices
    else:
        # forward fill an existing position
        b[time[-1]] = b[time[-2]]
        
portfolio = b.build()
portfolio.nav.plot()
# Trading only once a month can lead to days where 150k had to be reallocated
portfolio.turnover.iloc[1:].plot()

Why not resampling the prices?#

I don’t believe in bringing the prices to a monthly grid. This would render it hard to construct signals given the sparse grid. We stay on a daily grid and trade once a month.